package com.demo.topics.util;

import android.accounts.AccountManager;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.AsyncTask;
import android.util.Log;
import com.demo.topics.R;
import com.google.android.gms.auth.GoogleAuthException;
import com.google.android.gms.auth.GoogleAuthUtil;
import com.google.android.gms.auth.UserRecoverableAuthException;
import com.google.android.gms.common.AccountPicker;
import com.google.api.services.youtube.YouTubeScopes;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

/**
 * Class to manage our session. Should be a singleton.
 * <p/>
 * TODO refactor this to an interface
 *
 * @author Ikai Lan <ikai@google.com>
 */
public class SessionManager {
    private static final String TAG = SessionManager.class.getSimpleName();

    // Constants
    protected static final String AUTH_PREFERENCES_NAME = "auth.preferences";
    protected static final String AUTH_TOKEN = "authToken";

    // Activity results
    public static final int REQUEST_PICK_ACCOUNT = 1;
    public static final int REQUEST_USER_AUTHORIZATION = 2;

    private Context mContext;
    private SharedPreferences mPreferences;

    // Singleton instance
    private static SessionManager mSessionManager;

    // Listeners to notify when session state changes
    private List<SessionListener> mSessionObservers;


    /**
     * Public interface to be implemented by {@link Activity} classes that need to be notified on events, such
     * as login or logout.
     */
    public interface SessionListener {

        /**
         * On a successful login, this method is executed.
         */
        public void onLoggedIn(String accountName, String authToken);

        /**
         * On a successful logout, this method is executed.
         */
        public void onLoggedOut();

        // TODO add error handlers, like onLoginError, onLogoutError
    }

    /**
     * This method is used to return an instance of the SessionManager.
     *
     * @param context takes a context for the purposes of initializing a {@link SharedPreferences instance}
     * @return aninstance of the session manager
     */
    public synchronized static SessionManager getInstance(Context context) {
        if (mSessionManager == null) {
            mSessionManager = new SessionManager(context);

            // Initialize the observers list
        }
        return mSessionManager;
    }

    // Hide the default constructor
    private SessionManager(Context context) {
        mContext = context;
        mPreferences = context.getSharedPreferences(AUTH_PREFERENCES_NAME, Context.MODE_PRIVATE);

        mSessionObservers = new ArrayList<SessionListener>();
    }

    /**
     * This method starts the entire authentication flow. First, the user is
     * prompted to select an account on the phone. Once the account is selected,
     * the user should be prompted with a OAuth authorization screen. Once that
     * has been accepted, an API call is made to the YouTube API to fetch the
     * user's playlists.
     */
    public void startAuthFlow() {
        Log.d(TAG, "startAuthFlow() called");
        // Start the login process
        Intent accountChooserIntent = AccountPicker.newChooseAccountIntent(
                null, null,
                new String[]{GoogleAuthUtil.GOOGLE_ACCOUNT_TYPE}, true,
                mContext.getString(R.string.select_an_account), null, null, null);
        ((Activity) mContext).startActivityForResult(accountChooserIntent, REQUEST_PICK_ACCOUNT);
    }


    /**
     * Method to check for login status of the user.
     *
     * @return whether we think the user is logged in or not, generally
     *         dependent on whether we have a token or not
     */
    public boolean isUserLoggedIn() {
        // Check shared preferences for a token, if it exists we are considered
        // logged in
        // If it doesn't, we are not

        String authToken = mPreferences.getString(AUTH_TOKEN, null);
        String accountName = mPreferences.getString(AccountManager.KEY_ACCOUNT_NAME,
                null);
        return authToken != null && accountName != null;
    }

    /**
     * Destroys the session information for the user, effectively logging them out.
     */
    public boolean logout() {
        SharedPreferences.Editor editor = mPreferences.edit();
        editor.remove(AccountManager.KEY_ACCOUNT_NAME);
        editor.remove(AUTH_TOKEN);
        boolean logoutStatus = editor.commit();

        if (logoutStatus) {
            // Notify the listeners if the logout was successful
            for (SessionListener listener : mSessionObservers) {
                listener.onLoggedOut();
            }
        }
        return logoutStatus;
    }

    /**
     * Adds a listener to the list of observers to notify when session state changes.
     *
     * @param listener a listener to add to the list of listeners to notify
     */
    public void addSessionListener(SessionListener listener) {
        // TODO more methods to work with listeners, including removing and clearing listeners

        // Make sure we don't double add a listener
        if(!mSessionObservers.contains(listener)) {
            mSessionObservers.add(listener);
        }
    }

    /**
     * Get an auth token for making API calls.
     *
     * @return an auth token for API calls
     */
    public String getAuthToken() {
        return mPreferences.getString(AUTH_TOKEN, null);
    }

    /**
     * Get the account name associated with this auth token.
     *
     * @return the account name associated with this auth token
     */
    public String getAccountName() {
        return mPreferences.getString(AccountManager.KEY_ACCOUNT_NAME, null);
    }


    /**
     * Save an account name and auth token pair.
     *
     * @param accountName the account name associated with this auth token
     * @param authToken   the auth token, used to make API calls
     * @return save status; true if successful
     */
    public boolean save(String accountName, String authToken) {
        SharedPreferences.Editor editor = mPreferences.edit();
        editor.putString(AccountManager.KEY_ACCOUNT_NAME, accountName);
        editor.putString(AUTH_TOKEN, authToken);
        return editor.commit();
    }

    /**
     * This method fires off a background task that fetches an auth token,
     * storing it in a local preference when it is complete.
     *
     * @param accountName the account name to authenticate, fetch and store a token for
     */
    public void fetchAuthToken(String accountName) {
        Log.d(TAG, "fetchAuthToken() invoked");
        new FetchAuthTokenTask().execute(accountName);
    }

    /**
     * Static class used for storing account name and auth token pairs. Used for
     * cleaner code in the AsyncTask.
     *
     * @author Ikai Lan <ikai@google.com>
     */
    private static class AccountAuth {
        private String accountName;
        private String token;

        public String getAccountName() {
            return accountName;
        }

        public void setAccountName(String accountName) {
            this.accountName = accountName;
        }

        public String getToken() {
            return token;
        }

        public void setToken(String token) {
            this.token = token;
        }

        @Override
        public String toString() {
            return "AccountAuth [accountName=" + accountName + ", token="
                    + token + "]";
        }
    }

    /**
     * Asynchronous task that fetches an auth token and stores it in local
     * shared preference. TODO error handling with the UI, publish progress?
     *
     * @author Ikai Lan <ikai@google.com>
     */
    private class FetchAuthTokenTask extends
            AsyncTask<String, Void, AccountAuth> {

        /**
         * This method attempts to get an auth token from Google. If it turns
         * out the user hasn't authorized this call yet, another part of this
         * code should transparently try to recover and call this method again.
         *
         * @throws IllegalArgumentException if you call this with more than one account name. This
         *                                  should NEVER happen
         */
        @Override
        protected AccountAuth doInBackground(String... accounts) {
            if (accounts.length != 1) {
                throw new IllegalArgumentException(
                        "Fetch token task must be called with one account name.");
            }

            String accountName = accounts[0];

            try {
                String authToken = GoogleAuthUtil.getToken(mContext,
                        accountName, "oauth2:" + YouTubeScopes.YOUTUBE + " "
                        + YouTubeScopes.YOUTUBE_UPLOAD);

                AccountAuth auth = new AccountAuth();
                auth.setAccountName(accountName);
                auth.setToken(authToken);

                return auth;
            } catch (UserRecoverableAuthException e) {
                // This exception is thrown when a user can take an action to
                // recover from
                // an authorization error. Likely the auth token is expired or
                // the user has never
                // granted this application access. This is explained in more
                // detail here
                // http://developer.android.com/google/play-services/auth.html#handle
                Log.i(TAG,
                        "Got user recoverable error "
                                + e.getMessage()
                                + " likely as a result of needing authorization. Starting an authorization intent.");
                Intent authIntent = e.getIntent();

                // Pass in the account name for use from when we recover and
                // attempt to fetch the token again
                authIntent.putExtra(AccountManager.KEY_ACCOUNT_NAME,
                        accountName); // Just reuse a constant

                ((Activity) mContext).startActivityForResult(authIntent, SessionManager.REQUEST_USER_AUTHORIZATION);
            } catch (IOException e) {
                Log.e(TAG, "Got IOException:" + e.getMessage());
                e.printStackTrace();
            } catch (GoogleAuthException e) {
                Log.e(TAG, "Got GoogleAuthException:" + e.getMessage());
                e.printStackTrace();
            }
            return null;
        }

        /**
         * Takes the auth token, and, if it is not null, saves it. Updates the
         * UI afterwards with the user's playlists.
         *
         * @param authToken auth information containing an account name and an API
         *                  token
         */
        @Override
        protected void onPostExecute(AccountAuth authToken) {
            Log.d(TAG, "Completed background task. Received auth token:"
                    + authToken);
            // Did we get a token? If so:
            // Save the account name to a local preference
            // Save the token to a local preference
            // Reload the UI to show the playlist and fetch playlists from the
            // API server
            if (authToken != null) {
                boolean saveStatus = save(authToken.getAccountName(), authToken.getToken());

                if (!saveStatus) {
                    Log.e(TAG, "Unable to save account name and token");
                } else {
                    // We were able to log in. Notify the listeners
                    for(SessionListener listener : mSessionObservers) {
                        listener.onLoggedIn(authToken.getAccountName(), authToken.getToken());
                    }
                }
            } else {
                Log.i(TAG, "For any number of reasons, authToken is null.");
                // If not, we should show the user some kind of error
            }
        }
    }
}
